using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Windows.Forms;
using AsyncLibrary.StatusListener;

namespace AsyncLibrary.UI
{
  public class LoadingCircle : Control, IStatusListener
  {

    #region Private fields

    #region StylePresets enum

    public enum StylePresets
    {
      Custom,
      Default,
      Firefox,
      IE7,
    }

    #endregion

    #region Constants

    private const double NumberOfDegreesInCircle = 360;
    private const double NumberOfDegreesInHalfCircle = NumberOfDegreesInCircle / 2;
    private const int DefaultInnerCircleRadius = 15;
    private const int DefaultOuterCircleRadius = 16;
    private const int DefaultNumberOfSpoke = 52;
    private const int DefaultSpokeThickness = 5;

    private const int FireFoxInnerCircleRadius = 6;
    private const int FireFoxOuterCircleRadius = 7;
    private const int FireFoxNumberOfSpoke = 9;
    private const int FireFoxSpokeThickness = 4;

    private const int IE7InnerCircleRadius = 8;
    private const int IE7OuterCircleRadius = 9;
    private const int IE7NumberOfSpoke = 24;
    private const int IE7SpokeThickness = 4;

    #endregion

    private readonly Color _defaultColor = Color.LightSlateGray;
    private readonly Timer _timer;
    private double[] _angles;
    private PointF _centerPoint;
    private Color _color;
    private Color[] _colors;
    private int _innerCircleRadius;
    private bool _isTimerActive;
    private int _numberOfSpoke;
    private int _outerCircleRadius;
    private int _progressValue;
    private int _spokeThickness;
    private StylePresets _stylePreset;
    private double _maxValue;
    private double _percent;

    #endregion

    #region Constructors/Destructors

    /// <summary>
    /// Initializes a new instance of the <see cref="LoadingCircle"/> class.
    /// </summary>
    public LoadingCircle()
    {
      SetStyle(ControlStyles.UserPaint, true);
      SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
      SetStyle(ControlStyles.ResizeRedraw, true);
      SetStyle(ControlStyles.SupportsTransparentBackColor, true);

      _color = _defaultColor;

      GenerateColorsPallet();
      GetSpokesAngles();
      GetControlCenterPoint();

      _timer = new Timer();
      _timer.Tick += TimerTick;
      ActiveTimer();

      Resize += LoadingCircle_Resize;
    }

    #endregion

    #region Public properties

    /// <summary>
    /// Gets or sets the lightest color of the circle.
    /// </summary>
    /// <value>The lightest color of the circle.</value>
    [TypeConverter("System.Drawing.ColorConverter"),
     Category("LoadingCircle"),
     Description("Sets the color of spoke.")]
    public Color Color
    {
      get { return _color; }
      set
      {
        _color = value;

        GenerateColorsPallet();
        Invalidate();
      }
    }

    /// <summary>
    /// Gets or sets the outer circle radius.
    /// </summary>
    /// <value>The outer circle radius.</value>
    [Description("Gets or sets the radius of outer circle."),
     Category("LoadingCircle")]
    public int OuterCircleRadius
    {
      get
      {
        if (_outerCircleRadius == 0)
          _outerCircleRadius = DefaultOuterCircleRadius;

        return _outerCircleRadius;
      }
      set
      {
        _outerCircleRadius = value;
        Invalidate();
      }
    }

    /// <summary>
    /// Gets or sets the inner circle radius.
    /// </summary>
    /// <value>The inner circle radius.</value>
    [Description("Gets or sets the radius of inner circle."),
     Category("LoadingCircle")]
    public int InnerCircleRadius
    {
      get
      {
        if (_innerCircleRadius == 0)
          _innerCircleRadius = DefaultInnerCircleRadius;

        return _innerCircleRadius;
      }
      set
      {
        _innerCircleRadius = value;
        Invalidate();
      }
    }

    /// <summary>
    /// Gets or sets the number of spoke.
    /// </summary>
    /// <value>The number of spoke.</value>
    [Description("Gets or sets the number of spoke."),
     Category("LoadingCircle")]
    public int NumberSpoke
    {
      get
      {
        if (_numberOfSpoke == 0)
          _numberOfSpoke = DefaultNumberOfSpoke;

        return _numberOfSpoke;
      }
      set
      {
        if (_numberOfSpoke != value && _numberOfSpoke > 0)
        {
          _numberOfSpoke = value;
          GenerateColorsPallet();
          GetSpokesAngles();

          Invalidate();
        }
      }
    }

    /// <summary>
    /// Gets or sets a value indicating whether this <see cref="LoadingCircle"/> is active.
    /// </summary>
    /// <value><c>true</c> if active; otherwise, <c>false</c>.</value>
    [Description("Gets or sets the number of spoke."),
     Category("LoadingCircle")]
    public bool Active
    {
      get { return _isTimerActive; }
      set
      {
        _isTimerActive = value;
        ActiveTimer();
      }
    }

    /// <summary>
    /// Gets or sets the spoke thickness.
    /// </summary>
    /// <value>The spoke thickness.</value>
    [Description("Gets or sets the thickness of a spoke."),
     Category("LoadingCircle")]
    public int SpokeThickness
    {
      get
      {
        if (_spokeThickness <= 0)
          _spokeThickness = DefaultSpokeThickness;

        return _spokeThickness;
      }
      set
      {
        _spokeThickness = value;
        Invalidate();
      }
    }

    /// <summary>
    /// Gets or sets the rotation speed.
    /// </summary>
    /// <value>The rotation speed.</value>
    [Description("Gets or sets the rotation speed. Higher the slower."),
     Category("LoadingCircle")]
    public int RotationSpeed
    {
      get { return _timer.Interval; }
      set
      {
        if (value > 0)
          _timer.Interval = value;
      }
    }

    /// <summary>
    /// Quickly sets the style to one of these presets, or a custom style if desired
    /// </summary>
    /// <value>The style preset.</value>
    [Category("LoadingCircle"),
     Description("Quickly sets the style to one of these presets, or a custom style if desired"),
     DefaultValue(0)]
    public StylePresets StylePreset
    {
      get { return _stylePreset; }
      set
      {
        _stylePreset = value;

        switch (_stylePreset)
        {
          case  StylePresets.Custom:
            break;
          case StylePresets.Firefox:
            SetCircleAppearance(FireFoxNumberOfSpoke,
              FireFoxSpokeThickness, FireFoxInnerCircleRadius,
              FireFoxOuterCircleRadius);
            break;
          case StylePresets.IE7:
            SetCircleAppearance(IE7NumberOfSpoke,
              IE7SpokeThickness, IE7InnerCircleRadius,
              IE7OuterCircleRadius);
            break;
          case StylePresets.Default:
            SetCircleAppearance(DefaultNumberOfSpoke,
              DefaultSpokeThickness,
              DefaultInnerCircleRadius,
              DefaultOuterCircleRadius);
            break;
        }
      }
    }

    public bool ShowPercentage { get; set; }

    #endregion

    #region Non-Public Methods/Events/Indexers

    /// <summary>
    /// Handles the Resize event of the LoadingCircle control.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
    private void LoadingCircle_Resize(object sender, EventArgs e)
    {
      GetControlCenterPoint();
    }

    /// <summary>
    /// Handles the Tick event of the aTimer control.
    /// </summary>
    /// <param name="sender">The source of the event.</param>
    /// <param name="e">The <see cref="T:System.EventArgs"/> instance containing the event data.</param>
    private void TimerTick(object sender, EventArgs e)
    {
      _progressValue = ++_progressValue % _numberOfSpoke;
      Invalidate();
    }

    /// <summary>
    /// Raises the <see cref="E:System.Windows.Forms.Control.Paint"></see> event.
    /// </summary>
    /// <param name="e">A <see cref="T:System.Windows.Forms.PaintEventArgs"></see> that contains the event data.</param>
    protected override void OnPaint(PaintEventArgs e)
    {
      if (_numberOfSpoke > 0)
      {
        e.Graphics.SmoothingMode = SmoothingMode.HighQuality;

        int intPosition = _progressValue;
        for (int intCounter = 0; intCounter < _numberOfSpoke; intCounter++)
        {
          intPosition = intPosition % _numberOfSpoke;
          DrawLine(e.Graphics,
            GetCoordinate(_centerPoint, _innerCircleRadius, _angles[intPosition]),
            GetCoordinate(_centerPoint, _outerCircleRadius, _angles[intPosition]),
            _colors[intCounter], _spokeThickness);
          intPosition++;
        }
      }

      DrawPercentage(e);

      base.OnPaint(e);
    }

    private void DrawPercentage(PaintEventArgs e)
    {
      if (!ShowPercentage || _maxValue == 0) return;
      string percent = string.Format("{0:0}%", _percent);
      if (!string.IsNullOrEmpty(percent))
      {
        SizeF textSize = e.Graphics.MeasureString(percent, Font);
        float textX = (Width/2.0F) - (textSize.Width/2.0F);
        float textY = (Height/2.0F) - (textSize.Height/2.0F);
        StringFormat format = new StringFormat
          {
            Alignment = StringAlignment.Center,
            LineAlignment = StringAlignment.Center
          };

        RectangleF rectangle = new RectangleF(textX, textY, textSize.Width, textSize.Height);


        using (SolidBrush textBrush = new SolidBrush(ForeColor))
        {
          e.Graphics.DrawString(percent, Font, textBrush, rectangle, format);
        }
      }
    }

    // Overridden Methods 
    /// <summary>
    /// Retrieves the size of a rectangular area into which a control can be fitted.
    /// </summary>
    /// <param name="proposedSize">The custom-sized area for a control.</param>
    /// <returns>
    /// An ordered pair of type <see cref="T:System.Drawing.Size"></see> representing the width and height of a rectangle.
    /// </returns>
    public override Size GetPreferredSize(Size proposedSize)
    {
      proposedSize.Width = (_outerCircleRadius + _spokeThickness) * 2;

      return proposedSize;
    }

    // Methods
    /// <summary>
    /// Darkens a specified color.
    /// </summary>
    /// <param name="objColor">Color to darken.</param>
    /// <param name="intPercent">The percent of darken.</param>
    /// <returns>The new color generated.</returns>
    private Color Darken(Color objColor, int intPercent)
    {
      int intRed = objColor.R;
      int intGreen = objColor.G;
      int intBlue = objColor.B;
      return Color.FromArgb(intPercent, Math.Min(intRed, byte.MaxValue), Math.Min(intGreen, byte.MaxValue),
        Math.Min(intBlue, byte.MaxValue));
    }

    /// <summary>
    /// Generates the colors pallet.
    /// </summary>
    private void GenerateColorsPallet()
    {
      _colors = GenerateColorsPallet(_color, Active, _numberOfSpoke);
    }

    /// <summary>
    /// Generates the colors pallet.
    /// </summary>
    /// <param name="objColor">Color of the lightest spoke.</param>
    /// <param name="blnShadeColor">if set to <c>true</c> the color will be shaded on X spoke.</param>
    /// <param name="intNbSpoke">spoke number</param>
    /// <returns>An array of color used to draw the circle.</returns>
    private Color[] GenerateColorsPallet(Color objColor, bool blnShadeColor, int intNbSpoke)
    {
      var objColors = new Color[NumberSpoke];

      // Value is used to simulate a gradient feel... For each spoke, the 
      // color will be darken by value in intIncrement.
      var bytIncrement = (byte)(byte.MaxValue / NumberSpoke);

      //Reset variable in case of multiple passes
      byte PERCENTAGE_OF_DARKEN = 0;

      for (int intCursor = 0; intCursor < NumberSpoke; intCursor++)
      {
        if (blnShadeColor)
        {
          if (intCursor == 0 || intCursor < NumberSpoke - intNbSpoke)
            objColors[intCursor] = objColor;
          else
          {
            // Increment alpha channel color
            PERCENTAGE_OF_DARKEN += bytIncrement;

            // Ensure that we don't exceed the maximum alpha
            // channel value (255)
            if (PERCENTAGE_OF_DARKEN > byte.MaxValue)
              PERCENTAGE_OF_DARKEN = byte.MaxValue;

            // Determine the spoke forecolor
            objColors[intCursor] = Darken(objColor, PERCENTAGE_OF_DARKEN);
          }
        }
        else
          objColors[intCursor] = objColor;
      }

      return objColors;
    }

    /// <summary>
    /// Gets the control center point.
    /// </summary>
    private void GetControlCenterPoint()
    {
      _centerPoint = GetControlCenterPoint(this);
    }

    /// <summary>
    /// Gets the control center point.
    /// </summary>
    /// <returns>PointF object</returns>
    private PointF GetControlCenterPoint(Control _objControl)
    {
      return new PointF((float)_objControl.Width / 2, _objControl.Height / 2 - 1);
    }

    /// <summary>
    /// Draws the line with GDI+.
    /// </summary>
    /// <param name="_objGraphics">The Graphics object.</param>
    /// <param name="_objPointOne">The point one.</param>
    /// <param name="_objPointTwo">The point two.</param>
    /// <param name="_objColor">Color of the spoke.</param>
    /// <param name="_intLineThickness">The thickness of spoke.</param>
    private void DrawLine(Graphics _objGraphics, PointF _objPointOne, PointF _objPointTwo,
                          Color _objColor, int _intLineThickness)
    {
      using (var solidBrush = new SolidBrush(_objColor))
      {
        using (var objPen = new Pen(solidBrush, _intLineThickness))
        {
          objPen.StartCap = LineCap.Round;
          objPen.EndCap = LineCap.Round;
          _objGraphics.DrawLine(objPen, _objPointOne, _objPointTwo);
        }
      }
    }

    /// <summary>
    /// Gets the coordinate.
    /// </summary>
    /// <param name="_objCircleCenter">The Circle center.</param>
    /// <param name="_intRadius">The radius.</param>
    /// <param name="_dblAngle">The angle.</param>
    /// <returns></returns>
    private PointF GetCoordinate(PointF _objCircleCenter, int _intRadius, double _dblAngle)
    {
      double dblAngle = Math.PI * _dblAngle / NumberOfDegreesInHalfCircle;

      return new PointF(_objCircleCenter.X + _intRadius * (float)Math.Cos(dblAngle),
        _objCircleCenter.Y + _intRadius * (float)Math.Sin(dblAngle));
    }

    /// <summary>
    /// Gets the spokes angles.
    /// </summary>
    private void GetSpokesAngles()
    {
      _angles = GetSpokesAngles(NumberSpoke);
    }

    /// <summary>
    /// Gets the spoke angles.
    /// </summary>
    /// <param name="_intNumberSpoke">The number spoke.</param>
    /// <returns>An array of angle.</returns>
    private double[] GetSpokesAngles(int _intNumberSpoke)
    {
      var Angles = new double[_intNumberSpoke];
      double dblAngle = NumberOfDegreesInCircle / _intNumberSpoke;

      for (int shtCounter = 0; shtCounter < _intNumberSpoke; shtCounter++)
        Angles[shtCounter] = (shtCounter == 0 ? dblAngle : Angles[shtCounter - 1] + dblAngle);

      return Angles;
    }

    /// <summary>
    /// Actives the timer.
    /// </summary>
    private void ActiveTimer()
    {
      if (_isTimerActive)
        _timer.Start();
      else
      {
        _timer.Stop();
        _progressValue = 0;
      }

      GenerateColorsPallet();
      Invalidate();
    }

    /// <summary>
    /// Sets the circle appearance.
    /// </summary>
    /// <param name="numberSpoke">The number spoke.</param>
    /// <param name="spokeThickness">The spoke thickness.</param>
    /// <param name="innerCircleRadius">The inner circle radius.</param>
    /// <param name="outerCircleRadius">The outer circle radius.</param>
    public void SetCircleAppearance(int numberSpoke, int spokeThickness,
                                    int innerCircleRadius, int outerCircleRadius)
    {
      NumberSpoke = numberSpoke;
      SpokeThickness = spokeThickness;
      InnerCircleRadius = innerCircleRadius;
      OuterCircleRadius = outerCircleRadius;

      Invalidate();
    }

    #endregion

    #region IStatusListener implementation

    public void SetProgress(int value)
    {
      _percent = (value / _maxValue) * 100d;
    }

    public void SetProgresMax(int value)
    {
      _maxValue = value;
    }

    public void SetMode(StatusProgressMode value)
    {
    }

    public void SetStatus(string message)
    {
    }

    public void SetInfo(string text)
    {
    }

    public void FatalError(Exception ex)
    {
    }

    #endregion

  }
}